﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Reflection;
using CompositionPlugin.Primitives;

namespace CompositionPlugin.Service
{
    public abstract class ImportedPluginManagerBase : IImportedPluginManager
    {
        public ImportedPluginManagerBase(IDictionary<int, ImportedPluginTracer> tracers = null)
        {
            if (tracers != null)
                foreach (var keyValue in tracers)
                    this._tracers[keyValue.Key] = keyValue.Value;
            // check all tracers
            if (!this._tracers.ContainsKey((int)PluginShareMode.Instance))
                this._tracers[(int)PluginShareMode.Instance] = new InstanceImportedPluginTracer();
        }

        public virtual void Initialize() { }

        public ImportedPlugin<T> GetPlugin<T>(object instance, ImportedPluginContractMetadata metadata)
        {
            if (metadata == null)
                throw new ArgumentNullException("metadata");
            return this.GetPluginInstance<T, IDefaultMetadata>(instance, metadata).FirstOrDefault();
        }

        public ImportedPlugin<T, TMetadata> GetPlugin<T, TMetadata>(object instance, ImportedPluginContractMetadata metadata)
        {
            if (metadata == null)
                throw new ArgumentNullException("metadata");
            return this.GetPluginInstance<T, TMetadata>(instance, metadata).FirstOrDefault();
        }

        public IEnumerable<ImportedPlugin<T>> GetPlugins<T>(object instance, ImportedPluginContractMetadata metadata)
        {
            if (metadata == null)
                throw new ArgumentNullException("metadata");
            return this.GetPluginInstance<T, IDefaultMetadata>(instance, metadata);
        }

        public IEnumerable<ImportedPlugin<T, TMetadata>> GetPlugins<T, TMetadata>(object instance, ImportedPluginContractMetadata metadata)
        {
            if (metadata == null)
                throw new ArgumentNullException("metadata");
            return this.GetPluginInstance<T, TMetadata>(instance, metadata);
        }

        public event EventHandler<UndeterminedImportedPluginEventArgs> UndetermindImportedPlugin;

        public virtual void OnRuntimeDisposing() { }

        #region protected

        protected virtual IEnumerable<ImportedPlugin<T, TMetadata>> OnUndetermindImportedPlugin<T, TMetadata>(IEnumerable<ImportedPlugin<T, TMetadata>> plugins, ImportedPluginContractMetadata metadata)
        {
            if (this.UndetermindImportedPlugin != null)
            {
                UndeterminedImportedPluginEventArgs eventArgs = new UndeterminedImportedPluginEventArgs(plugins.Select(p=>new ExportPartDefinition(p.Plugin, p.Metadata)), metadata);
                this.UndetermindImportedPlugin(this, eventArgs);
                // check
                return eventArgs.SelectedExports.Select(p => new ImportedPlugin<T, TMetadata>((T)p.Plugin, (TMetadata)p.Metadata));
            }
            else
            {
                return plugins;
            }
        }

        protected virtual ImportedPluginTracer GetPluginTracer(ImportedPluginContractMetadata metadata)
        {
            return this._tracers[(int)metadata.ShareMode];
        }

        protected virtual IEnumerable<ImportedPlugin<T, TMetadata>> GetPluginInstance<T, TMetadata>(object instance, ImportedPluginContractMetadata metadata)
        {
            // try get from tracer
            var tracer = this.GetPluginTracer(metadata);
            var plugins = tracer.GetPlugins<T, TMetadata>(instance, metadata);
            if (plugins == null || plugins.Count() == 0)
            {
                var exportedPlugins = this.ExportPlugin<T, TMetadata>(metadata);
                if (exportedPlugins == null || exportedPlugins.Count() == 0)
                {
                    return Enumerable.Empty<ImportedPlugin<T, TMetadata>>();
                }
                else
                {
                    tracer.Trace(instance, metadata, exportedPlugins);
                    return exportedPlugins;
                }
            }
            else
            {
                return plugins;
            }
        }

        protected virtual IEnumerable<ImportedPlugin<T, TMetadata>> ExportPlugin<T, TMetadata>(ImportedPluginContractMetadata metadata)
        {
            var exports = SystemRuntime.Current.Composition.Container.GetExports<T, TMetadata>();
            var plugins = exports != null ? exports.Select(p => new ImportedPlugin<T, TMetadata>(p.Value, p.Metadata)).ToArray() : Enumerable.Empty<ImportedPlugin<T, TMetadata>>().ToArray();
            // verify cardinatlity
            if (plugins.Length > 1 && (metadata.Cardinality == ImportCardinality.ExactlyOne || metadata.Cardinality == ImportCardinality.ZeroOrOne))
                plugins = this.OnUndetermindImportedPlugin<T, TMetadata>(plugins, metadata).ToArray();
            if (plugins.Length > 1 && (metadata.Cardinality == ImportCardinality.ExactlyOne || metadata.Cardinality == ImportCardinality.ZeroOrOne))
                throw new ImportCardinalityMismatchException();
            else if(plugins.Length < 1 && metadata.Cardinality == ImportCardinality.ExactlyOne)
                throw new ImportCardinalityMismatchException();
            // return
            return plugins;
        }

        #endregion

        #region private

        private readonly Dictionary<int, ImportedPluginTracer> _tracers = new Dictionary<int, ImportedPluginTracer>();

        private Lazy<T> WrapperToLazy<T>(object value)
        {
            if (value is T)
                return new Lazy<T>(new Func<T>(() => (T)value));
            else
                throw new ArgumentException("Value type mismatch");
        }

        private Lazy<T, TMetadata> WrapperToLazy<T, TMetadata>(object value, object metadata)
        {
            if (!(value is T))
                throw new ArgumentException("Value type mismatch");
            if (!(metadata is TMetadata))
                throw new ArgumentException("Metadata type mismatch");
            return new Lazy<T, TMetadata>(new Func<T>(() => (T)value), (TMetadata)metadata);
        }

        #endregion
    }
}
